package pt.fct.mpc.lastfm.geo;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.LinkedList;
import java.util.List;

import pt.fct.mpc.lastfm.LastfmPreferences;
import pt.fct.mpc.lastfm.R;
import pt.fct.mpc.lastfm.connectivity.LastfmBluetoothUnavailableException;
import pt.fct.mpc.lastfm.connectivity.bt.LastfmBluetoothManager;
import pt.fct.mpc.lastfm.event.EventData;
import pt.fct.mpc.lastfm.generic.ImageActivity;
import pt.fct.mpc.lastfm.search.LastfmSearchEvents;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.DialogInterface.OnClickListener;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.location.Address;
import android.location.Geocoder;
import android.location.Location;
import android.location.LocationManager;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.AsyncTask.Status;
import android.provider.Settings;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.ImageView;
import android.widget.Toast;

import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapActivity;
import com.google.android.maps.MapController;
import com.google.android.maps.MapView;
import com.google.android.maps.MyLocationOverlay;
import com.google.android.maps.Overlay;
import com.google.android.maps.OverlayItem;

public class LastfmGeoEventsActivity
	extends MapActivity
{
	private static final String TAG = "Last.fm Geo Location";
	
	private MapView map_view;
	private LocationManager location_manager;
	private MapController map_controller;
	private LastfmGeoEventsOverlay overlay_events;
	private MyLocationOverlay overlay_my_location;
	private Context context;
	private List<Overlay> view_overlays;
	
	private ProgressDialog searching_dialog;
	private Handler gui_handler = new Handler();
	
	private GeoPoint last_known_location;
	private boolean find_location = true;
	private Bundle single_event_bundle = null;

	private boolean show_static_map = false;
	private ImageView static_map_view = null;

	private EventData[] nearby_events = null;

	private boolean using_bt;

	private EventSearchTask events_search_task = null;

	@Override
	protected void onCreate(Bundle icicle)
	{
		super.onCreate(icicle);
		setContentView(R.layout.lastfm_map);
		
		Log.d(TAG, "onCreate()");

		Intent our_intent = getIntent();
		Bundle our_intent_extras = our_intent.getExtras();
		
		if (our_intent_extras != null)
		{
			if (isCorrectEventBundle(our_intent_extras))
			{
				find_location = false;
				this.single_event_bundle = our_intent_extras;
			}
		}
		
		setTitle(R.string.lastfm_geomap_title);

		// Allow receiving an intent with an event's address and use Geocoder
		// to obtain its coordinates.
		
		this.context = getApplicationContext();
		
		this.map_view = (MapView) findViewById(R.id.layout_geomap_view);
		this.map_view.setBuiltInZoomControls(true);
		this.map_view.setTraffic(false);
		
		this.map_controller = this.map_view.getController();
		this.view_overlays = this.map_view.getOverlays();
		this.last_known_location = null;

		this.location_manager = 
			(LocationManager) getSystemService(LOCATION_SERVICE);
		
		if (this.location_manager == null)
		{
			Log.e(TAG, "Location Manager is null");
			finish();
		}
		
		boolean gps_enabled = 
			location_manager.isProviderEnabled(LocationManager.GPS_PROVIDER);
		
		if ((gps_enabled == false) && (find_location == true))
		{
			Intent enable_gps_intent = 
				new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
			
			startActivity(enable_gps_intent);
		}
		
		// recheck it
		gps_enabled = 
			location_manager.isProviderEnabled(LocationManager.GPS_PROVIDER);
		if (gps_enabled == false)
			finish();
		
		this.using_bt = LastfmBluetoothManager.isOn();

		setupSearchingDialog();

		if (this.find_location == true)
			setupOverlayMyLocation();
		
		if (this.using_bt == false)
		{
			setupOverlayEvents();
		
			if (this.find_location == false)
				setupSingleEvent();
		}
		
		if (this.using_bt == true)
			setupStaticMap();
		
		if ((this.find_location == false) && (this.using_bt == true))
			showStaticMap();
	}
	
	@Override
	public boolean onCreateOptionsMenu(Menu menu)
	{
		MenuInflater inflater = getMenuInflater();
		inflater.inflate(R.menu.map_options_menu, menu);
		return true;
	}
	
	@Override
	public boolean onPrepareOptionsMenu(Menu menu)
	{
		MenuItem static_map_item = 
			(MenuItem) menu.findItem(R.id.maps_options_menu_show_static);
		MenuItem dynamic_map_item =
			(MenuItem) menu.findItem(R.id.maps_options_menu_show_dynamic);
		
		boolean static_visibility = true, dynamic_visibility = false;
    	
    	if (this.show_static_map)
    	{
    		static_visibility = false;
    		dynamic_visibility = true;
    	}
    	static_map_item.setVisible(static_visibility);
    	dynamic_map_item.setVisible(dynamic_visibility);

    	MenuItem shown_map_item = 
    		(static_visibility ? static_map_item : dynamic_map_item);
    	
    	shown_map_item.setEnabled((using_bt ? false : true));
		
		return true;
	}
	
	@Override
	public boolean onOptionsItemSelected(MenuItem item)
	{
		switch (item.getItemId()) 
		{
			case R.id.maps_options_menu_show_static:

				setupStaticMap();
				showStaticMap();
				break;
		}

		return true;
	}
	
	@Override
	protected void onResume()
	{
		super.onResume();

		if (this.find_location == false)
			return;
		
		this.overlay_my_location.enableCompass();
		
		if (this.last_known_location != null)
			return; // We don't need to do anything more for the time being.
		
		this.searching_dialog.show();

		this.last_known_location = 
			getGeoPointFromLocation(
				this.location_manager.getLastKnownLocation(
					LocationManager.GPS_PROVIDER));
		
		
		boolean ok = this.overlay_my_location.enableMyLocation();
		if (!ok)
		{
			if (this.searching_dialog.isShowing())
				this.searching_dialog.dismiss();
			
			AlertDialog.Builder builder = new AlertDialog.Builder(this);
			builder.setMessage("It is not possible to acquire GPS signal.")
				.setCancelable(false)
				.setPositiveButton("OK", 
					new OnClickListener()
					{
						@Override
						public void onClick(DialogInterface dialog, int which)
						{
							dialog.dismiss();
						}
					});
			AlertDialog alert = builder.create();
			alert.show();
		}
	}
	
	@Override
	protected void onPause()
	{
		super.onPause();

		if (this.events_search_task != null)
		{
			if (this.events_search_task.getStatus() != Status.FINISHED)
			{
				this.events_search_task.cancel(true);
				this.events_search_task = null;
			}
		}
		
		if (this.find_location == false)
			return;
		
		this.overlay_my_location.disableCompass();
		this.overlay_my_location.disableMyLocation();
	}

	@Override
	protected boolean isRouteDisplayed()
	{
		return false;
	}

	private boolean isCorrectEventBundle(Bundle bundle)
	{
		boolean ret = false;
		
		do
		{
			if (bundle == null)
				break;
			
			if (!bundle.containsKey("artist") || !bundle.containsKey("date")
				|| !bundle.containsKey("title") 
				|| !bundle.containsKey("latitude") 
				|| !bundle.containsKey("longitude"))
				break;
			
			ret = true;
			
		} while (false);
		
		return ret;
	}

	private void showStaticMap()
	{
		Log.d(TAG, "Showing Static Map");
		
		// XXX
		// This is a verification created after a random exception occurred.
		// We have no idea why the exception happened and we were unable to
		// reproduce it. The only thing we know for sure is that this method
		// was called without the static map view being initialized, which
		// probably means it weren't meant to be used, and caused a
		// NullPointerException to be thrown. We hereby set this verification
		// to make sure we force the usage of the dynamic map if the static
		// map is not to be used, hoping this avoids the situation just
		// described.
		if (this.show_static_map == false)
			showDynamicMap();
		
		GeoStaticMap map = new GeoStaticMap();

		if ((this.find_location == false) 
			&& (this.single_event_bundle != null))
		{
			String lat = this.single_event_bundle.getString("latitude");
			String lng = this.single_event_bundle.getString("longitude");
			
			GeoPoint point = getGeoPointFromLocation(lat, lng);
			
			map.setCenter(point);
			map.addMarker(point);
		}
		else
		{
			if (this.last_known_location != null)
			{
				map.setCenter(this.last_known_location);
				
				for (EventData e : this.nearby_events)
					map.addMarker(e.getGeoPoint());
			} else
			{
				Toast.makeText(context, "Current location unknown", 
					Toast.LENGTH_LONG).show();
				return;
			}
		}
		
		String base_url = "http://maps.google.com/maps/api/staticmap";
		String params = map.getParameters();
		
		String full_url = base_url + params;
		
		Bitmap static_map = ImageActivity.urlToImage(full_url);
		
		if (static_map == null)
		{
			Log.e(TAG, "Static Maps: null image");
			return;
		}
		
		this.static_map_view.setImageBitmap(static_map);
	}
	
	private void showDynamicMap()
	{
		Log.d(TAG, "Showing dynamic map");
		for (EventData e : this.nearby_events)
		{
			GeoPoint p = e.getGeoPoint();
			if (p == null)
				continue;
			
			OverlayItem item = new OverlayItem(p, e.getTitle(), 
				e.getArtist() + " on "+ e.getDate());
			this.overlay_events.addToOverlay(item);
		}
		
		Log.d(TAG, "Events looped through.");
		
		if (this.searching_dialog.isShowing())
			this.searching_dialog.dismiss();
		
		this.map_controller.setZoom(12);
	}
	
	private void showMap()
	{
		Log.d(TAG, "Showing map");
		if (this.show_static_map)
			showStaticMap();
		else
			showDynamicMap();
	}

	private void setupStaticMap()
	{
		this.show_static_map = true;
		
		this.map_view.setVisibility(View.GONE);
		
		this.static_map_view = 
			(ImageView) findViewById(R.id.layout_geomap_image_view);
		this.static_map_view.setVisibility(View.VISIBLE);		
	}

	private void setupSingleEvent()
	{
		setTitle("Event Location");
		String lat = this.single_event_bundle.getString("latitude");
		String lng = this.single_event_bundle.getString("longitude");
		
		String title = this.single_event_bundle.getString("title");
		String artist = this.single_event_bundle.getString("artist");
		String date = this.single_event_bundle.getString("date");
		
		GeoPoint event_point = getGeoPointFromLocation(lat, lng);
		
		Log.d(TAG, "single event geopoint = "+event_point);
		if (event_point == null)
		{
			Log.d(TAG, "single event geopoint is null");
			return;
		}
		
		OverlayItem item = new OverlayItem(event_point, title, 
			artist + " on " + date);
		
		this.overlay_events.addToOverlay(item);
		this.map_controller.setZoom(18);
		this.map_controller.animateTo(event_point);
	}

	private void setupOverlayMyLocation()
	{
		this.overlay_my_location = 
			new MyLocationOverlay(context, this.map_view);
		
		this.overlay_my_location.runOnFirstFix(
			new Runnable()
			{
				@Override
				public void run()
				{
					map_controller.setZoom(LastfmPreferences.DEFAULT_ZOOM_LEVEL);
					last_known_location = overlay_my_location.getMyLocation();
					map_controller.animateTo(last_known_location);
					overlay_my_location.disableMyLocation();
					
					gui_handler.post(
						new Runnable()
						{
							@Override
							public void run()
							{
								if (searching_dialog.isShowing())
									searching_dialog.dismiss();

								doAcquireEvents();
							}
						});

				}
			});
		this.view_overlays.add(this.overlay_my_location);
	}

	private void setupOverlayEvents()
	{
		Drawable marker =
			context.getResources().getDrawable(R.drawable.icon_lastfm_event);
		Log.d(TAG, "marker = "+marker);
		this.overlay_events = new LastfmGeoEventsOverlay(this, marker);
		this.view_overlays.add(this.overlay_events);
	}

	private void setupSearchingDialog()
	{
		this.searching_dialog = new ProgressDialog(this);
		this.searching_dialog.setMessage("Acquiring GPS signal...");
		this.searching_dialog.setButton(DialogInterface.BUTTON_NEUTRAL, 
			"Cancel", new OnClickListener()
			{
				@Override
				public void onClick(DialogInterface dialog, int which)
				{
					dialog.dismiss();
					overlay_my_location.disableMyLocation();
					
					if (last_known_location == null)
					{
						Toast.makeText(context, "Current location is unknown.\n"
							+"Unable to show events.", Toast.LENGTH_LONG).show();
						return;
					}
					
					map_controller.animateTo(last_known_location);
					doAcquireEvents();
				}
			});		
	}

	private GeoPoint getGeoPointFromLocation(Location lastFix)
	{
		if (lastFix == null)
		{
			Log.e(TAG, "Last Fix is null");
			return null;
		}
		
		int lat = (int) (lastFix.getLatitude() * 1E6);
		int lng = (int) (lastFix.getLongitude() * 1E6);
		GeoPoint point = new GeoPoint(lat, lng);
		
		return point;
	}

	private GeoPoint getGeoPointFromLocation(String lat, String lng)
	{
		double d_lat = Double.parseDouble(lat);
		double d_lng = Double.parseDouble(lng);
		
		int i_lat = (int) (d_lat * 1E6);
		int i_lng = (int) (d_lng * 1E6);
		
		return new GeoPoint(i_lat, i_lng);
	}

	private void doAcquireEvents()
	{
		this.events_search_task = new EventSearchTask();
		this.events_search_task.execute(this.last_known_location);
	}
	
	
	class GeoStaticMap
	{
		private static final String FORMAT = "png";
		private static final int ZOOM_SINGLE = 18;
		private static final int ZOOM_MULTIPLE = 
			LastfmPreferences.DEFAULT_ZOOM_LEVEL;
		
		private GeoPoint center;
		private List<GeoPoint> markers;
		
		private int size_height;
		private int size_width;

		private int zoom = 0;
	
		public GeoStaticMap()
		{
			this.size_height = 
				getWindowManager().getDefaultDisplay().getHeight();
			this.size_width = 
				getWindowManager().getDefaultDisplay().getWidth();
			
			this.markers = new LinkedList<GeoPoint>();
		}
		
		public void setCenter(GeoPoint point)
		{
			this.center = point;
		}
		
		public void setZoom(int zoom)
		{
			this.zoom = zoom;
		}
		
		public void addMarker(GeoPoint marker)
		{
			this.markers.add(marker);
		}
		
		public String getParameters()
		{
			String params = "";
			
			if (this.markers.size() > 1)
			{
				String icon_url = "http://chart.apis.google.com/chart";
				String center_marker_url_params =
					"?chst=d_map_pin_icon"
					+"&chld=location|3D9AE6";
				String center_marker_url_params_encoded = "";
				String center_marker_icon_str = "";
				try
				{
					center_marker_url_params_encoded = 
						URLEncoder.encode(center_marker_url_params, "UTF-8");
					center_marker_icon_str = 
						"icon:"+icon_url+center_marker_url_params_encoded+"|";
				} catch (UnsupportedEncodingException e)
				{
					e.printStackTrace();
				}
				
				String center_marker = 
					"?markers="+center_marker_icon_str
						+ geopointToDoubleString(this.center);
				
				Log.d(TAG, "center marker = "+center_marker);
				
				params += center_marker;
			}
			else
				params += "?center="+geopointToDoubleString(this.center);
			
			boolean show_zoom = (this.markers.size() > 1 ? false : true);
			if (this.zoom != 0)
				show_zoom = true;
				
			int final_zoom = 0;
			
			if (show_zoom)
			{
				final_zoom = (this.zoom != 0 ? this.zoom 
				: (this.markers.size() > 1 ? ZOOM_MULTIPLE : ZOOM_SINGLE));
					
				params += "&zoom="+final_zoom;
			}
			
			params += "&size="+this.size_width+"x"+this.size_height;
			params += "&format="+FORMAT;
			// We have enough screen precision, we don't need the mobile version.
			params += "&mobile=false";
			params += "&sensor="+(find_location ? "true" : "false");
			
			if (this.markers.size() > 0)
			{
				params += "&markers=";

				int i = 0;
				for (GeoPoint p : this.markers)
				{
					if ((i ++) > 0)
						params += "|";
					params += geopointToDoubleString(p);
				}
			}

			return params;
		}

		private String geopointToDoubleString(GeoPoint point)
		{
			double lat = point.getLatitudeE6() / 1E6;
			double lng = point.getLongitudeE6() / 1E6;
			
			return new String(lat + "," + lng);
		}
	}
	
	class EventSearchTask
		extends AsyncTask<GeoPoint, Void, EventData[]>
	{
		@Override
		protected void onPreExecute()
		{
			super.onPreExecute();
			
			Log.d(TAG, "Starting onPreExecute()");
			
			if (searching_dialog != null && searching_dialog.isShowing())
				searching_dialog.dismiss();
			
			searching_dialog = new ProgressDialog(LastfmGeoEventsActivity.this);
			searching_dialog.setMessage("Searching for nearby events...");
			searching_dialog.show();
			
			Log.d(TAG, "Finishing onPreExecute()");
		}

		@Override
		protected EventData[] doInBackground(GeoPoint... params)
		{
			Address addr = null;
			GeoPoint location = (GeoPoint) params[0];
			
			if (LastfmPreferences.USE_GEOCODING)
			{
				Geocoder geocoder = new Geocoder(context);
				double lat = location.getLatitudeE6() / 1E6;
				double lng = location.getLongitudeE6() / 1E6;
				
				Log.d(TAG, "Geocoding for coordinates: "+lat+","+lng);
				try
				{
					List<Address> location_lst = 
						geocoder.getFromLocation(lat, lng, 1);
					if (location_lst.size() <= 0)
						return null;
					
					
					addr = location_lst.get(0);
					if (!addr.hasLatitude() || !addr.hasLongitude())
						return null;
				} catch (IOException e)
				{
					// TODO Auto-generated catch block
					e.printStackTrace();
					return null;
				}
			}
			
			LastfmSearchEvents events_search = 
				new LastfmSearchEvents(context);
			
			Log.d(TAG, "Starting doInBackground()");
			
			try
			{
				if (LastfmPreferences.USE_GEOCODING)
					nearby_events = events_search.getEvents(addr);
				else
					nearby_events = events_search.getEvents(location);
			} catch (LastfmBluetoothUnavailableException e)
			{
				e.printStackTrace();
				Toast.makeText(context, "Problems with bluetooth!",
					Toast.LENGTH_SHORT);
				return null;
			}
			
			Log.d(TAG, "Finishing doInBackground()");
			
			return nearby_events;
		}
		
		@Override
		protected void onPostExecute(EventData[] result)
		{
			super.onPostExecute(result);
			
			Log.d(TAG, "Starting onPostExecute()");
			
			if (searching_dialog.isShowing())
				searching_dialog.dismiss();
			
			if (nearby_events == null)
			{
				Toast.makeText(context, "Failed retrieving events", 
					Toast.LENGTH_LONG).show();
				
				return;
			}
			else
				Log.d(TAG, "Great success!");
			
			Log.d(TAG, "Finishing onPostExecute()");
			
			showMap();
			events_search_task = null;
		}
		
	}
	

}
